# Python for S60 application sending raw acceleration data 
# as JSON string to predefined host and port.
# Intended for use over WLAN (no latency)
# ball drawing from Sensor Sample (Jouni Miettunen, 2008)

import e32
import appuifw
import graphics
import key_codes
import socket
import btsocket
import urllib
import thread

# Set up acceleration sensor
# only run on devices where acceleration sensor data is available
import sensor 
sensors = sensor.sensors()
sensor_data = sensors['AccSensor'] 
sensor_acc = sensor.Sensor(sensor_data['id'], 
                           sensor_data['category'])
sensor_acc.set_event_filter(sensor.EventFilter())

# States
sensor_active = sender_active = True
remote_image = False


# what to send down
JSON_TEMPLATE = '{"x":%d , "y":%d, "z":%d, "k":"%s", "id": %d}'


class Sender(object):
    "sends a text packet to host:port"

    def __init__(self):
        self.sock = None
        self.bt_sock = None
        self.host = None
        self.port = None
        self.bluetooth = False
        self.sending = False

    def setHostPort(self, host, port):
        "sets host:port, handles bluetooth"
        self.sending = False
        if ":" in host: # bluetooth, more complicated
            self.close_bluetooth()
            self.init_bluetooth(host, port)
            if self.bluetooth:
                self.host = host
                self.port = port
        else: # socket communication, simple
            self.close_bluetooth()
            # UDP connection
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.bluetooth = False
            self.host = host
            self.port = port
        self.sending = True

    def close_bluetooth(self):
        "tries to close bluetooth if already open"
        if self.bluetooth:
            self.bluetooth = False
        if self.bt_sock:
            try:
                self.bt_sock.close()
            except Exception, msg:
                print "closing failed", msg
            self.bt_sock = None

    def init_bluetooth(self, host, port):
        "tries to initialize bluetooth and sets to bluetooth if ok"
        if self.bt_sock != None:
            return # ok, already initialized
        try:
            self.bt_sock = self.bt_connect((host, port))
            if self.bt_sock:
                self.bluetooth = True
        except Exception, msg:
            print msg

    def bt_connect(self, address):
        # adapted from bluetoothconsole 
        btsock = btsocket.socket(btsocket.AF_BT, btsocket.SOCK_STREAM)
        # Discovery doesn't work with 1.9.7
        # no idea why; but it crashes horribly; anyway - the address must
        # be already known then
        # print "BTC: Connecting to "+str(address)+"...",
        try:
            btsock.connect(address)
        except socket.error, err:
            if err[0]==54: # "connection refused"
                print "BTC: 54 Connection refused."
                return None
            if err[0]==61: # "connection refused"
                print "BTC: 61 Connection refused"                
            raise
        # print "BTC: OK"
        return btsock

        
    def __call__(self, data, end="\n"):
        "sends packet with either nice socket or horrible bluetooth"
        if not self.host or not self.port:
            print "Sender host:port not set"
            return
        if not self.sending:
            # print "skipping data", data
            return
        if self.bluetooth:
            if self.bt_sock:
                self.bt_sock.send(data + end)
        else:
            self.sock.sendto(data + end, (self.host, self.port))


sender = Sender()
# Offered IP addresses and ports to send UDP data to
addrs = [("150.162.202.87",    9617), # lab machine cable
         ("150.162.202.69",    9617), # lab machine WLAN
         ("192.168.0.185",     9617), # lab machine local WLAN
         ("00:19:7D:EA:FA:9B",   17), # lab machine Bluetooth
         ]
sender.setHostPort(*addrs[1]) # select default host:port

def set_hostport(host, port):
    "set host and port of sender"
    sender.setHostPort(host, port)
    menu_init()

def addset_hostport():
    "interactively enter host:port and add to offered ones"
    res = appuifw.query(u"host:port", 
                        "text", 
                        unicode(sender.host)+u":"+unicode(sender.port))
    if not res and ":" not in res:
        return
    parts = map(lambda s:s.strip(), res.split(":"))
    nhost = ":".join(parts[:-1])
    nport = parts[-1]
    try:
        nport = int(nport)
    except Exception, data:
        print data
        return
    addrs.append((nhost, nport))
    sender.setHostPort(nhost, nport)


class Keyboard(object):
    "http://snippets.dzone.com/posts/show/1444"
    def __init__(self):
        self.state = {}  # is this key pressing ?
        self.buffer= {}  # is it waiting to be processed ?
    def handle_event(self, event): # for event_callback
        code = event['scancode']
        if event['type'] == appuifw.EEventKeyDown:
            self.buffer[code]= 1   # put into queue
            self.state[code] = 1
        elif event['type'] == appuifw.EEventKeyUp:
            self.state[code] = 0
    def pressing(self, code):      # just check
        return self.state.get(code,0)
    def pressed(self, code):       # check and process the event
        if self.buffer.get(code,0):
            self.buffer[code] = 0  # take out of queue
            return 1
        return 0

def make_coordinate(a_data, a_screen):
    ''' Convert sensor data to screen coordinate '''
    return a_screen - ((a_data + 300) * a_screen) / 600

check_keys = {
    key_codes.EScancodeDownArrow : "d",
    key_codes.EScancodeUpArrow : "u",
    key_codes.EScancodeLeftArrow : "l",
    key_codes.EScancodeRightArrow : "r",
    key_codes.EScancodeSelect : "f",
    key_codes.EScancode0 : "0",
    key_codes.EScancode1 : "1",
    key_codes.EScancode2 : "2",
    key_codes.EScancode3 : "3",
    key_codes.EScancode4 : "4",
    key_codes.EScancode5 : "5",
    key_codes.EScancode6 : "6",
    key_codes.EScancode7 : "7",
    key_codes.EScancode8 : "8",
    key_codes.EScancode9 : "9",
    key_codes.EScancodeStar : "*",
    key_codes.EScancodeHash : "#",
}

packet_id = 0 # global packet_id to number sent packets
def handle_sensor_raw(data):
    global packet_id
    d1, d2, d3 = data['data_1'], data['data_2'], data['data_3']
    packet_id += 1
    if sender_active:
        # print "sender active", packet_id
        keys = ""
        for key in check_keys:
            if keyboard.pressing(key):
                keys += check_keys[key]
        # d2 is x and d1 is y! 
        # also turning it left should be negative as is turning towards you
        packet = JSON_TEMPLATE % (-d2, d1, d3, keys, packet_id)
        sender(packet)
    if not remote_image: # raw data
        # print "local display", packet_id
        # data_1 for y-axis, 2 for x, 3 for dot size
        x = make_coordinate(d2, screen_x)
        y = make_coordinate(d1, screen_y)
        w = (d3 + 300) / 2
        draw_dot(x, y, w)
        cb_handle_redraw()
    else: # remote image NOT!
        # IT IS ABSOLUTELY IMPOSSIBLE TO RELIABLE DRAW 
        # AN IMAGE ON THE CANVAS WHERE THE IMAGE HAS BEEN
        # RETRIEVED WITHIN A THREAD. EVEN THE RETRIEVING
        # IS TOTALLY MESSY, AS IT NEEDS A TEMPORARY FILE.
        # I GIVE UP TRYING TO DO ANYTHING SENSIBLE WITH
        # THREADS IN A SYMBIAN ENVIRONMENT!!!!!        
        if packet_id % 40 == 0:
            draw_text(20,20,u"Sent " + unicode(packet_id))
            cb_handle_redraw()



def draw_dot(x, y, w, c=(255,255,255), o=(0,0,255)):
    "Draw point on-screen"
    img.clear(c)
    img.point((x, y), width=w, outline=o)

def draw_text(x, y, text):
    img.clear((255,255,255))
    img.text((x, y), text)
    
def cb_handle_redraw(dummy=(0, 0, 0, 0)):
    "Overwrite default screen redraw event handler"
    # print "cb_handle_redraw"
    if img:
        canvas.blit(img)

def set_remote_image(val):
    "whether to show remote image or raw data"
    global remote_image
    if remote_image == val:
        return
    remote_image = val
    menu_init()
    # JUST NO WAY TO DO IT
    #if remote_image:
        #start_retrieving()
    #else:
        #stop_retrieving()


tmp_file = "d:\\download.jpg"
def retrieve_one():
    global img
    url = "http://150.162.202.69:8082/bla.png?a=1"
    socket.setdefaulttimeout(1) # hopefully http timeout after a sec
    try:
        urllib.urlretrieve(url, tmp_file)
    except:
        print "possibly timed out"
        return
    tmp_img = graphics.Image.open(tmp_file)
    print img.size, tmp_img.size
    print "got one"
    # THERE IS NO FREAKING WAY TO JUST PAINT THIS
    # DAMN IMAGE ON THE CANVAS WITHOUT BLOCKING 
    # THE APPLICATION OR CRASHING IT. TRYING 
    # ANY COMBINATION OF AO_SLEEP AND AO_YIELD AND
    # WHATNOT TO NO EFFECT. PLEASE SOMEBODY MAKE
    # THE SENSOR STUFF FOR THE N95 AVAILABLE FROM
    # WITHIN JAVA; PLEASE PLEASE. I GIVE UP HERE
    # img = tmp_img
    # img_dirty = True
    # e32.ao_sleep(0.1)
    # img = tmp_img # just use that one (That is what goes wrong)
    # ok draw into the image
    #img_dirty = True
    #img.blit(tmp_img)
    # print "callgating"
    # e32.ao_callgate(lambda: blit_image(img, tmp_img))()
    # print "this would be the horrible part that goes wrong"
    # cb_handle_redraw()

rid = 0
def retriever():
    global rid
    rid += 1
    lrid = rid
    a_round = 0
    while remote_image and lrid == rid:
        # e32.ao_sleep(0.1) # is this really! needed? (actually no!)
        print "a round in retriever", a_round
        a_round += 1
        retrieve_one()
        continue
        try:
            retrieve_one()
        except socket.timeout:
            print "socket_timeout"
            pass # ignore timeout exception
        except IOError, data:
            # actually pys60 does not throw a socket.timeout but IOError
            print data
            pass # ignore timeout exception
        except Exception: 
            print "other exception"
            raise
            lrid = -1 # silently exit

thread_id = 0
def start_retrieving():
    global thread_id
    thread_id = thread.start_new_thread(retriever, ())

def stop_retrieving():
    global rid
    rid += 1 # forces exit on the next round anyway
    
    

def menu_init():
    "Initialize and update menu"
    t_sens = u"Off" if sensor_active else "On"
    t_send = u"Off" if sender_active else "On"
    con_entries = ()
    for shost, sport in addrs:
        text = u"*" if shost==sender.host and sport==sender.port else u" "
        text += u"_%s:%d" % (shost, sport)
        func = lambda sh=shost, sp=sport: set_hostport(sh,sp)
        con_entries = con_entries + ((text, func),)
    view_entries = ()
    t = u" " if remote_image else u"*"
    view_entries += ((t+u"Raw Sensor", 
                      lambda: set_remote_image(False)),)
    t = u"*" if remote_image else u" "
# NOT WITH SYMBIAN EVER!!!
#    view_entries += ((t+u"Image HTTP", 
#                      lambda: set_remote_image(True)),)
    view_entries += ((t+u"Status Only", 
                      lambda: set_remote_image(True)),)
    appuifw.app.menu = [
        (u"Sensor %s" % t_sens, menu_sensor_toggle),
        (u"Sender %s" % t_send, menu_sender_toggle),
        (u"View", view_entries),
        (u"Connection",
         (con_entries + ((u"Add", lambda: addset_hostport()),))),
        (u"About", menu_about),
        (u"Exit", cb_quit)
        ]

def menu_about():
    appuifw.note(u"Send Acceleration Sensor Data via UDP")


def menu_sensor_toggle():
    ''' Toggle sensor (accelerator) on/off '''
    global sensor_active
    sensor_active = not sensor_active
    if sensor_active:        
        sensor_acc.connect(handle_sensor_raw)
    else:
        sensor_acc.disconnect()
    menu_init()

def menu_sender_toggle():
    ''' Toggle sending on/off '''
    global sender_active
    sender_active = not sender_active
    menu_init()


def cb_quit():
    ''' Clean-up before exit '''
    if sensor_active:
        sensor_acc.disconnect()
    stop_retrieving() # ok, even if not retrieving right now
    app_lock.signal()


### application
appuifw.app.orientation = 'portrait'
appuifw.app.screen = 'large'
appuifw.app.title = u'Sensor Sender'
appuifw.app.exit_key_handler = cb_quit
menu_init()

keyboard = Keyboard()
canvas = appuifw.Canvas(redraw_callback=cb_handle_redraw,
                        event_callback=keyboard.handle_event)
screen_x, screen_y = canvas.size
img = graphics.Image.new(canvas.size) # global img to draw to
appuifw.app.body = canvas

# send data initially
sensor_acc.connect(handle_sensor_raw)

# event loop
app_lock = e32.Ao_lock()
app_lock.wait() 
